/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.abm.jwebsite.sitemap;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.abm.jwebsite.exception.ErrorPagesMissingInSiteMapException;
import org.abm.jwebsite.exception.JWebsiteException;
import org.abm.jwebsite.exception.SiteMapXMLNotCorrectlyStructuredException;
import org.abm.jwebsite.exception.SitemapContainsDuplicatePageIDException;
import org.abm.jwebsite.exception.WebsitePropertiesNotProperlySetException;
import org.abm.jwebsite.utilities.Constants;
import static org.abm.jwebsite.utilities.Constants.CNST_DISPLAY;
import static org.abm.jwebsite.utilities.Constants.CNST_FIRST_ITEM_IN_ARRAY;
import static org.abm.jwebsite.utilities.Constants.CNST_HREF;
import static org.abm.jwebsite.utilities.Constants.CNST_KEY_SITEMAP_XML;
import static org.abm.jwebsite.utilities.Constants.CNST_PAGE_ID;
import static org.abm.jwebsite.utilities.Constants.CNST_TITLE;
import static org.abm.jwebsite.utilities.Constants.CNST_XPATH_SITEMAP_EXPR_PAGES;
import org.abm.jwebsite.utilities.Utilities;
import org.abm.jwebsite.utilities.XMLParser;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 *
 * @author Saptarshi Debnath (saptarshi.devnath@gmail.com)
 */
public enum SiteMapImpl implements SiteMap, Constants {

    INSTANCE;
    private Utilities utils = null;
    private String[] sitemapSeo = null;
    private String[] sitemapNavigation = null;
    private String[] sitemapNavigationData = null;
    private String[] sitemapData = null;
    private Navigation pages = null;
    private ArrayList<Navigation> orphan = null;
    private Map<String, Navigation> pageIdToNavigationMap = null;
    private Map<String, Navigation> hrefToNavigationMap = null;
    private String siteMenu;
    private boolean siteMenuIsAjax;

    private SiteMapImpl() {
        try {
            this.init();
        } catch (XPathExpressionException | SAXException | ParserConfigurationException | IOException ex) {
            throw new JWebsiteException(ex);
        }
    }

    private void init() throws XPathExpressionException, SAXException, ParserConfigurationException, IOException {
        this.siteMenu = null;
        this.utils = Utilities.INSTANCE;
        this.pageIdToNavigationMap = new HashMap<>();
        this.hrefToNavigationMap = new HashMap<>();
        this.parseSiteMapXML();
        this.pageIdToNavigationMap = Collections.unmodifiableMap(this.pageIdToNavigationMap);
        this.utils.log(Level.INFO, "Sitemap initialized successfully" + this.utils.getLineSeperator() + this.pages.toString() + this.utils.getLineSeperator() + this.orphan.toString());
    }

    private void parseSiteMapXML() throws XPathExpressionException, SAXException, ParserConfigurationException, IOException {
        //
        // Read the setting
        //
        String xmlFile = this.utils.getRealPath(this.utils.getProperty(CNST_KEY_SITEMAP_XML));
        this.sitemapSeo = this.utils.getProperty(CNST_KEY_SITEMAP_SEO).split(CNST_SPLITTER);
        this.sitemapNavigation = this.utils.getProperty(CNST_KEY_SITEMAP_NAVIGATION).split(CNST_SPLITTER);
        this.sitemapNavigationData = this.utils.getProperty(CNST_KEY_SITEMAP_NAVIGATION_DATA).split(CNST_SPLITTER);
        this.sitemapData = this.utils.getProperty(CNST_KEY_SITEMAP_DATA).split(CNST_SPLITTER);

        //
        // Validate the settings
        //
        this.validateWebsiteProperties(sitemapSeo, CNST_SITEMAP_SEO_MANDATORY, CNST_KEY_SITEMAP_SEO);
        this.validateWebsiteProperties(sitemapNavigation, CNST_SITEMAP_NAVIGATION_MANDATORY, CNST_KEY_SITEMAP_NAVIGATION);
        this.validateWebsiteProperties(sitemapNavigationData, CNST_SITEMAP_NAVIGATION_DATA_MANDATORY, CNST_KEY_SITEMAP_NAVIGATION_DATA);
        this.validateWebsiteProperties(sitemapData, CNST_SITEMAP_DATA_MANDATORY, CNST_KEY_SITEMAP_DATA);

        //
        // Parsing the XML File multiple times to read the Content.
        //
        NodeList nodeList = XMLParser.parse(new File(xmlFile), "/site-map");
        if (nodeList.getLength() != CNST_COUNT_ONE) {
            throw new SiteMapXMLNotCorrectlyStructuredException();
        } else {
            String ajax = XMLParser.getAttributeValue(nodeList.item(CNST_FIRST_ITEM_IN_ARRAY), "ajax");
            if (ajax.length() > CNST_MIN_STRING_LENGTH) {
                this.siteMenuIsAjax = Boolean.parseBoolean(ajax);
            }
        }
        nodeList = XMLParser.parse(new File(xmlFile), CNST_XPATH_SITEMAP_EXPR_PAGES);
        if (nodeList.getLength() != CNST_COUNT_ONE) {
            throw new SiteMapXMLNotCorrectlyStructuredException();
        } else {
            this.pages = this.populateNavigation(nodeList.item(CNST_FIRST_ITEM_IN_ARRAY));
        }
        nodeList = XMLParser.parse(new File(xmlFile), CNST_XPATH_SITEMAP_EXPR_ORPHAN);
        if (nodeList.getLength() != CNST_COUNT_ONE) {
            throw new SiteMapXMLNotCorrectlyStructuredException();
        } else {
            NodeList orphanNavs = nodeList.item(CNST_FIRST_ITEM_IN_ARRAY).getChildNodes();
            int countOrphanNavs = orphanNavs.getLength();
            this.orphan = new ArrayList<>(countOrphanNavs);
            Node currentItem = null;
            for (int i = 0; i < countOrphanNavs; i++) {
                currentItem = orphanNavs.item(i);
                if (currentItem.getNodeName().equalsIgnoreCase(CNST_SITEMAP_NAVIGATION_NODE_NAME)) {
                    this.orphan.add(this.populateNavigation(currentItem));
                }
            }
        }
        this.checkErrorPages();
    }

    private void checkErrorPages() {
        Set<String> tempSet = this.pageIdToNavigationMap.keySet();
        if (!tempSet.contains(CNST_NAVIGATION_ERROR_404) || !tempSet.contains(CNST_NAVIGATION_ERROR_500)) {
            throw new ErrorPagesMissingInSiteMapException();
        }
    }

    private Navigation populateNavigation(Node navigation) {
        Map<String, String> attributesSeo = XMLParser.getAttributeValue(navigation, sitemapSeo);
        Map<String, String> attributesNavigation = XMLParser.getAttributeValue(navigation, sitemapNavigation);
        Map<String, String> attributesNavigationData = XMLParser.getAttributeValue(navigation, sitemapNavigationData);
        Map<String, String> attributesData = XMLParser.getAttributeValue(navigation, sitemapData);

        //
        // Add the context path with the HREF value
        //
        attributesNavigation.put(CNST_HREF, this.utils.getContextPath() + attributesNavigation.get(CNST_HREF));

        NodeList childNavigationList = navigation.getChildNodes();
        int childNavigationCount = childNavigationList.getLength();
        List<Navigation> childNavigation = new ArrayList<>();
        Node currentItem = null;
        for (int i = 0; i < childNavigationCount; i++) {
            currentItem = childNavigationList.item(i);
            if (currentItem.getNodeName().equalsIgnoreCase(CNST_SITEMAP_NAVIGATION)) {
                childNavigation.add(this.populateNavigation(currentItem));
            }
        }
        Navigation nav = new Navigation(attributesSeo, attributesNavigation, attributesNavigationData, attributesData, childNavigation);
        //
        // One time setting of the parent in the Navigation. So it is a complte tree.
        //
        for (Navigation tempNav : childNavigation) {
            tempNav.setParent(nav);
        }
        String pageIDLocalCache = attributesNavigationData.get(CNST_PAGE_ID);
        String hrefLocalCache = attributesNavigation.get(CNST_HREF);
        if (!this.isPageIDAlreadyUsed(pageIDLocalCache)) {
            this.pageIdToNavigationMap.put(pageIDLocalCache, nav);
        } else {
            throw new SitemapContainsDuplicatePageIDException(this.utils.getProperty(CNST_KEY_SITEMAP_XML) + " contains duplicate page id : " + pageIDLocalCache + ". Please use unique page id for every page/navigation node");
        }
        if (!this.isHREFAlreadyUsed(hrefLocalCache)) {
            this.hrefToNavigationMap.put(hrefLocalCache, nav);
        } else {
            throw new SitemapContainsDuplicatePageIDException(this.utils.getProperty(CNST_KEY_SITEMAP_XML) + " contains duplicate href : " + hrefLocalCache + ". Please use unique href for every page/navigation node");
        }
        return nav;
    }

    private boolean isPageIDAlreadyUsed(String pageID) {
        return (this.pageIdToNavigationMap.keySet().contains(pageID));
    }

    private boolean isHREFAlreadyUsed(String href) {
        return (this.hrefToNavigationMap.keySet().contains(href));
    }

    private void validateWebsiteProperties(String[] readValue, String[] expectedValue, String key) {
        StringBuilder buffer = new StringBuilder();
        for (String temp1 : expectedValue) {
            boolean found = false;
            for (String temp2 : readValue) {
                buffer.append(temp2).append(CNST_SPLITTER);
                if (temp1.equalsIgnoreCase(temp2)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                throw new WebsitePropertiesNotProperlySetException("The key \"" + key + "\" should include the field " + temp1 + ". Eg : \"" + key + "=" + buffer.toString() + temp1 + "\"");
            }
        }
    }

    @Override
    public String getTitleAndSeo(String pageID) {

        Navigation tempNavgation = this.pageIdToNavigationMap.get(pageID);
        String metaDescriptionContent = null;
        if (tempNavgation != null) {
            StringBuilder builder = new StringBuilder();
            builder.append(this.utils.getLineSeperator());

            //
            // build the meta description
            //
            for (Map.Entry<String, String> entry : tempNavgation.getSeo().entrySet()) {
                if (entry.getValue() != null && entry.getValue().length() <= 0) {
                    continue;
                } else if (entry.getKey().equalsIgnoreCase(CNST_TITLE)) {
                    builder.append("<")
                            .append(entry.getKey())
                            .append(">")
                            .append(entry.getValue())
                            .append("</")
                            .append(entry.getKey())
                            .append(">")
                            .append(this.utils.getLineSeperator());
                } else {
                    builder.append("<meta name=\"")
                            .append(entry.getKey())
                            .append("\" content=\"")
                            .append(entry.getValue())
                            .append("\" />")
                            .append(this.utils.getLineSeperator());
                }
            }

            //
            // Add the robots entry
            //
            if (this.utils.isProductionEnvironment() && this.utils.getProjectMode().equals(CNST_PROJECT_MODE_PRODUCTION)) {
                builder.append("<META NAME=\"ROBOTS\" CONTENT=\"INDEX, FOLLOW\">");
            } else {
                builder.append("<META NAME=\"ROBOTS\" CONTENT=\"NOINDEX, NOFOLLOW\">");
            }

            //
            // Add the charset description
            //
            builder.append(this.utils.getLineSeperator())
                    .append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=")
                    .append(this.utils.getPageEnconding())
                    .append("\">")
                    .append(this.utils.getLineSeperator());

            //
            // Add the site icon
            //
            builder.append("<link rel=\"icon\" type=\"image/ico\" href=\"")
                    .append(this.utils.getWebsiteIcon())
                    .append("\">")
                    .append(this.utils.getLineSeperator());
            //
            // Append pageID as javascript varaible
            //
            builder.append(this.utils.getLineSeperator())
                    .append("<script type=\"text/javascript\">(function(abm, undefined) {").append(this.utils.getLineSeperator())
                    .append("	var data = {};").append(this.utils.getLineSeperator())
                    .append("	data.ajaxParamName='").append(this.utils.getProperty(Constants.CNST_AJAX_PARAM_NAME)).append("';").append(this.utils.getLineSeperator())
                    .append("	data.ajaxParamContentValue='").append(this.utils.getProperty(Constants.CNST_AJAX_PARAM_VALUE_CONTENT)).append("';").append(this.utils.getLineSeperator())
                    .append("	data.pageid='").append(pageID).append("';").append(this.utils.getLineSeperator())
                    .append("   window.abm.data = data;").append(this.utils.getLineSeperator())
                    .append("}(window.abm = window.abm || {}));").append("</script>").append(this.utils.getLineSeperator())
                    .append(this.utils.getLineSeperator());

            metaDescriptionContent = builder.toString();
            this.utils.log(Level.INFO, this.utils.getLineSeperator() + "Meta Descriton for page id : " + pageID + this.utils.getLineSeperator() + metaDescriptionContent);
        }
        return metaDescriptionContent;
    }

    @Override
    public String getTitleAndSeo(HttpServletRequest request) {
        return this.getTitleAndSeo(utils.getPageID(request));
    }

    @Override
    public String getHREF(String pageID) {
        String href = this.pageIdToNavigationMap.get(pageID).getNavigation(CNST_HREF);
        this.utils.log(Level.INFO, this.utils.getLineSeperator() + "HREF generated for page id : " + pageID + this.utils.getLineSeperator() + href);
        return href;
    }

    @Override
    public String getAnchorTag(String pageID) {
        StringBuilder buffer = new StringBuilder();
        buffer.append("<a");
        Navigation tempNavigation = this.pageIdToNavigationMap.get(pageID);
        for (Map.Entry<String, String> entry : tempNavigation.getNavigation().entrySet()) {
            if (entry.getValue() != null && entry.getValue().length() > 0) {
                buffer.append(" ")
                        .append(entry.getKey())
                        .append("=\"")
                        .append(entry.getValue())
                        .append("\" ");
            }
        }
        buffer.append(" >")
                .append(tempNavigation.getNavigationData().get(CNST_LABEL))
                .append(" </a>");
        String anchorTag = buffer.toString();
        this.utils.log(Level.INFO, this.utils.getLineSeperator() + "Anchor tag generated for page id : " + pageID + this.utils.getLineSeperator() + anchorTag);
        return anchorTag;
    }

    @Override
    public Navigation getNavigationByHREF(String href) {
        return this.hrefToNavigationMap.get(href.trim());
    }

    @Override
    public String getpageIDByHREF(String href) {
        Navigation nav = this.getNavigationByHREF(href);
        String pageID = null;
        if (nav != null) {
            pageID = nav.getNavigationData(CNST_PAGE_ID);
        }
        this.utils.logInfo("HREF : " + href + " is mapped to page id : " + pageID);
        return pageID;
    }

    @Override
    public String printMenu(HttpServletRequest request) {
        if (this.siteMenu == null) {
            String pageID = (String) request.getAttribute(CNST_PAGE_ID);
            StringBuilder buffer = new StringBuilder();
            buffer.append("<nav class=\"mainMenu\">");
            this.generateMenu(buffer, this.pages);
            buffer.append("</nav>");
            this.siteMenu = buffer.toString();
        }
        return this.siteMenu;
    }

    private void generateMenu(StringBuilder buffer, Navigation nav) {
        Iterator<Navigation> child = nav.getChildNavigation();
        if (Boolean.parseBoolean(nav.getData().get(CNST_DISPLAY))) {
            String id = nav.getNavigationData().get(CNST_PAGE_ID);
            buffer.append("<div class=\"menu\">")
                    .append("<div class=\"link\" id=\"pageid-")
                    .append(id)
                    .append("\"><p>")
                    .append(this.getAnchorTag(nav.getNavigationData().get(CNST_PAGE_ID)))
                    .append("</p></div>")
                    .append("<div class=\"subpages\">");
            while (child.hasNext()) {
                this.generateMenu(buffer, child.next());
            }
            buffer.append("</div>").append("</div>");
        }
    }
}
